/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.physics;

import cg2.mathematics.MathVector;
import cg2.mathematics.Mathematics;

/**
 * @brief A simple physical object that maintains its velocity, acceleration, position and has a mass.
 * 
 * One can also apply forces/accelerations to physical objects. Furthermore, by setting the friction coefficient the
 * movement will take this effect into account.
 */
public abstract class PhysicalObject {
	public final double mass;
	protected MathVector velocity;
	protected MathVector position;
	protected MathVector acceleration;
	protected double     time_lastClimpse;
	protected double     time_elapsed;
	protected double     frictionCoefficient;
	protected boolean    paused      = true;
	protected boolean    paused_time = true;
	
	/**
	 * @brief Constructs a object with the given mass.
	 */
	public PhysicalObject(double mass) {
		this.mass     = mass;
		velocity      = new MathVector(3);
		position      = new MathVector(3);
		acceleration  = new MathVector(3);
		time_lastClimpse = System.currentTimeMillis();
	}
	
	/**
	 * @brief The given force will be applied to this object (beginning with the call of this method). 
	 * @throws IllegalArgumentException if the dimension of force does not equal 3.
	 */
	public void applyForce(MathVector force) {
		if (force.dim != 3)
			throw new IllegalArgumentException("Given force vector does not consist of 3 components.");
		acceleration.add(Mathematics.mul(force, 1./mass));
	}
	
	/**
	 * @brief The given acceleration will be applied to this object (beginning with the call of this method).
	 * @throws IllegalArgumentException if the dimension of acceleration does not equal 3.
	 */
	public void applyAcceleration(MathVector acceleration) {
		if (acceleration.dim != 3)
			throw new IllegalArgumentException("Given acceleration vector does not consist of 3 components.");
		this.acceleration.add(acceleration);
	}
	
	/**
	 * @brief Returns the friction coefficient.
	 */
	public double getFrictionCoefficient() {
		return frictionCoefficient;
	}
	
	/**
	 * @brief Returns a vector representing the current position of this object.
	 */
	public MathVector getPosition() {
		return position.clone();
	}
	
	/**
	 * @brief Returns a vector representing the current velocity of this object.
	 */
	public MathVector getVelocity() {
		return velocity.clone();
	}
	
	/**
	 * @brief Pauses this object.
	 * @see update()
	 */
	public void pause() {
		paused = true;
	}
	
	/**
	 * @brief In contrast to pause(), this method will not only stop updating the object, but also 'stop time', such
	 *        that upon resuming, the object behaves as if no time had elapsed.
	 */
	public void pause_time() {
		paused_time = true;
	}
	
	/**
	 * @brief Resumes this object.
	 * @see update()
	 */
	public void resume() {
		if (paused_time)
			time_lastClimpse = System.currentTimeMillis();
		paused      = false;
		paused_time = false;
	}
	
	/**
	 * @brief Sets the acceleration.
	 */
	public void setAcceleration(double a1, double a2, double a3) {
		acceleration.set(0, a1);
		acceleration.set(1, a2);
		acceleration.set(2, a3);
	}
	
	/**
	 * @brief Sets the acceleration.
	 */
	public void setAcceleration(MathVector acceleration) {
		if (acceleration.dim != 3)
			throw new IllegalArgumentException("Given acceleration vector does not have dimension 3.");
		this.acceleration = acceleration.clone();
	}
	
	/**
	 * @brief Sets the friction coefficient.
	 */
	public void setFrictionCoefficient(double value) {
		frictionCoefficient = value;
	}
	
	/**
	 * @brief Sets the position.
	 */
	public void setPosition(double x, double y, double z) {
		position.set(0, x);
		position.set(1, y);
		position.set(2, z);
	}
	
	/**
	 * @brief Sets the position.
	 */
	public void setPosition(MathVector position) {
		if (position.dim != 3)
			throw new IllegalArgumentException("Given position vector does not have dimension 3.");
		this.position = position.clone();
	}
	
	/**
	 * @brief Sets the velocity.
	 */
	public void setVelocity(double v1, double v2, double v3) {
		velocity.set(0, v1);
		velocity.set(1, v2);
		velocity.set(2, v3);
	}
	
	/**
	 * @brief Sets the velocity.
	 */
	public void setVelocity(MathVector velocity) {
		if (velocity.dim != 3)
			throw new IllegalArgumentException("Given velocity vector does not have dimension 3.");
		this.velocity = velocity.clone();
	}
	
	/**
	 * @brief Updates the position of this object according to the elapsed time since last update.
	 * 
	 * If this method is called on an paused object, nothing will happen.
	 */
	public void update() {
		// return if object was paused
		if (paused || paused_time)
			return;
		
		long currTime    = System.currentTimeMillis();
		time_elapsed     = (double)(currTime-time_lastClimpse) / 1000.;
		time_lastClimpse = currTime;
		
		// update position
		MathVector oldPosition = position.clone();
		position.add(Mathematics.mul(velocity,     time_elapsed                ));
		position.add(Mathematics.mul(acceleration, time_elapsed*time_elapsed/2.));

		// update velocity
		velocity.add(Mathematics.mul(acceleration, time_elapsed));
		
		// compute effect of friction
		MathVector motion_vector = Mathematics.sub(position, oldPosition);
		double     motion_length = motion_vector.norm();
		if (motion_length != 0. && frictionCoefficient != 0.) {
			motion_vector.normalize();
			
			// get acceleration due to friction
			double friction_acceleration = frictionCoefficient*Physics.gravity_earth;
			
			// take friction into account (position)
			double friction_posCoeff = Math.min(motion_length, friction_acceleration/2. * time_elapsed*time_elapsed);
			position.sub(Mathematics.mul(motion_vector, friction_posCoeff));
			
			// take friction into account (velocity)
			double friction_velCoeff = friction_acceleration*time_elapsed;
			if (friction_velCoeff > velocity.norm()) {
				velocity.set(0, 0.);
				velocity.set(1, 0.);
				velocity.set(2, 0.);
			} else {
				velocity.sub(Mathematics.mul(motion_vector, friction_velCoeff));
			}
		}
	}
}
